/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.meego.common.memcache;

import com.meego.common.config.Config;
import com.meego.common.log.GA;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.cliffc.high_scale_lib.NonBlockingHashMap;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;

/**
 *
 * @author TueHM
 */
public class RedisClient {

    private static final Lock createLock_ = new ReentrantLock();
    private static Map<String, RedisClient> instances = new NonBlockingHashMap();
    private ArrayBlockingQueue<Jedis> pooledJedis;
    private String host;
    private Integer port;
    private Integer maxPool;
    private Integer minPool;
    private Integer timeout = Integer.valueOf(5000);

    public static RedisClient getInstance(String instanceName) {
        if (!instances.containsKey(instanceName)) {
            try {
                createLock_.lock();
                if (!instances.containsKey(instanceName)) {
                    instances.put(instanceName, new RedisClient(instanceName));
                }
            } finally {
                createLock_.unlock();
            }
        }
        return (RedisClient) instances.get(instanceName);
    }

    public RedisClient(String instanceName) {
        this.host = Config.getStrConfig(instanceName + "host", "");
        this.port = Config.getIntConfig(instanceName + "port", 4730);
        this.maxPool = Config.getIntConfig(instanceName + "maxpool", 1024);
        this.minPool = Config.getIntConfig(instanceName + "minpool", 256);
        this.pooledJedis = new ArrayBlockingQueue(this.maxPool.intValue());
    }

    private Jedis borrowClient() {
        Jedis client = null;

        while (this.pooledJedis.size() > 0) {
            synchronized (RedisClient.class) {
                try {
                    client = (Jedis) this.pooledJedis.take();                    
                } catch (InterruptedException ex) {
                    GA.cache.error("Exception in borrow", ex);
                }
            }
            if(client.isConnected() && client.ping().equals("PONG")){
                break;
            }
        }
        if (client == null) {
            client = new Jedis(this.host, this.port.intValue(), this.timeout.intValue());
            client.connect();
        }

        return client;
    }

    private void returnClient(Jedis client) {
        try {
            if ((client != null) && (this.pooledJedis.size() <= this.maxPool.intValue())) {
                this.pooledJedis.put(client);
            } else {
                destroyClient(client);
            }
        } catch (InterruptedException e) {
            GA.cache.error("Exception in put return client : ", e);
        }
    }

    private void invalidClient(Jedis client) {
        try {
            if ((client != null) && (client.isConnected()) && (client.ping().equals("PONG"))) {
                returnClient(client);
            } else {
                destroyClient(client);
            }
        } catch (Exception ex) {
        }
    }

    private void destroyClient(Jedis client) {
        if (client != null) {
            client.shutdown();
            client = null;
        }
    }

    public Long lpush(String key, String[] strings) throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Long result = client.lpush(key, strings);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Set the string value as value of the key. The string can't be longer than 1073741824 bytes (1 GB). <p> Time
     * complexity: O(1)
     *
     * @param key
     * @param value
     * @return Status code reply
     */
    public boolean set(String key, String value)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            boolean result = "OK".equals(client.set(key, value));
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    public long setnx(String key, String value)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            long result = client.setnx(key, value).longValue();
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Get the value of the specified key. If the key does not exist the special value 'nil' is returned. If the value
     * stored at key is not a string an error is returned because GET can only handle string values. <p> Time
     * complexity: O(1)
     *
     * @param key
     * @return Bulk reply
     */
    public String get(String key)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            String result = client.get(key);
            returnClient(client);
            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Get the value of the specified key. If the key does not exist the special value 'nil' is returned. If the value
     * stored at key is not a string an error is returned because GET can only handle string values. <p> Time
     * complexity: O(1)
     *
     * @param key
     * @return Bulk reply
     */
    public List<String> get(String[] keys)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            List result = client.mget(keys);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Return the specified elements of the list stored at the specified key. Start and end are zero-based indexes. 0 is
     * the first element of the list (the list head), 1 the next element and so on. <p> For example LRANGE foobar 0 2
     * will return the first three elements of the list. <p> start and end can also be negative numbers indicating
     * offsets from the end of the list. For example -1 is the last element of the list, -2 the penultimate element and
     * so on. <p> <b>Consistency with range functions in various programming languages</b> <p> Note that if you have a
     * list of numbers from 0 to 100, LRANGE 0 10 will return 11 elements, that is, rightmost item is included. This may
     * or may not be consistent with behavior of range-related functions in your programming language of choice (think
     * Ruby's Range.new, Array#slice or Python's range() function). <p> LRANGE behavior is consistent with one of Tcl.
     * <p> <b>Out-of-range indexes</b> <p> Indexes out of range will not produce an error: if start is over the end of
     * the list, or start > end, an empty list is returned. If end is over the end of the list Redis will threat it just
     * like the last element of the list. <p> Time complexity: O(start+n) (with n being the length of the range and
     * start being the start offset)
     *
     * @param key
     * @param start
     * @param end
     * @return Multi bulk reply, specifically a list of elements in the specified range.
     */
    public List<String> lrange(String key, long start, long end)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            List result = client.lrange(key, start, end);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Increment the number stored at key by one. If the key does not exist or contains a value of a wrong type, set the
     * key to the value of "0" before to perform the increment operation. <p> INCR commands are limited to 64 bit signed
     * integers. <p> Note: this is actually a string operation, that is, in Redis there are not "integer" types. Simply
     * the string stored at the key is parsed as a base 10 64 bit signed integer, incremented, and then converted back
     * as a string. <p> Time complexity: O(1)
     *
     * @see #incrBy(String, long)
     * @see #decr(String)
     * @see #decrBy(String, long)
     *
     * @param key
     * @return Integer reply, this commands will reply with the new value of key after the increment.
     */
    public Long incr(String key)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Long result = client.incr(key);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * INCRBY work just like {@link #incr(String) INCR} but instead to increment by 1 the increment is integer. <p> INCR
     * commands are limited to 64 bit signed integers. <p> Note: this is actually a string operation, that is, in Redis
     * there are not "integer" types. Simply the string stored at the key is parsed as a base 10 64 bit signed integer,
     * incremented, and then converted back as a string. <p> Time complexity: O(1)
     *
     * @see #incr(String)
     * @see #decr(String)
     * @see #decrBy(String, long)
     *
     * @param key
     * @param integer
     * @return Integer reply, this commands will reply with the new value of key after the increment.
     */
    public Long incrBy(String key, long by)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Long result = client.incrBy(key, by);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Decrement the number stored at key by one. If the key does not exist or contains a value of a wrong type, set the
     * key to the value of "0" before to perform the decrement operation. <p> INCR commands are limited to 64 bit signed
     * integers. <p> Note: this is actually a string operation, that is, in Redis there are not "integer" types. Simply
     * the string stored at the key is parsed as a base 10 64 bit signed integer, incremented, and then converted back
     * as a string. <p> Time complexity: O(1)
     *
     * @see #incr(String)
     * @see #incrBy(String, long)
     * @see #decrBy(String, long)
     *
     * @param key
     * @return Integer reply, this commands will reply with the new value of key after the increment.
     */
    public Long decr(String key)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Long result = client.decr(key);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * IDECRBY work just like {@link #decr(String) INCR} but instead to decrement by 1 the decrement is integer. <p>
     * INCR commands are limited to 64 bit signed integers. <p> Note: this is actually a string operation, that is, in
     * Redis there are not "integer" types. Simply the string stored at the key is parsed as a base 10 64 bit signed
     * integer, incremented, and then converted back as a string. <p> Time complexity: O(1)
     *
     * @see #incr(String)
     * @see #decr(String)
     * @see #incrBy(String, long)
     *
     * @param key
     * @param integer
     * @return Integer reply, this commands will reply with the new value of key after the increment.
     */
    public Long decrBy(String key, long by) throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Long result = client.decrBy(key, by);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Add the specified member having the specifeid score to the sorted set stored at key. If member is already a
     * member of the sorted set the score is updated, and the element reinserted in the right position to ensure
     * sorting. If key does not exist a new sorted set with the specified member as sole member is crated. If the key
     * exists but does not hold a sorted set value an error is returned. <p> The score value can be the string
     * representation of a double precision floating point number. <p> Time complexity O(log(N)) with N being the number
     * of elements in the sorted set
     *
     * @param key
     * @param score
     * @param member
     * @return Integer reply, specifically: 1 if the new element was added 0 if the element was already a member of the
     * sorted set and the score was updated
     */
    public Long zadd(String key, double score, String member)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Long result = client.zadd(key, score, member);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Adds all the specified members with the specified scores to the sorted set stored at key. It is possible to
     * specify multiple score/member pairs. If a specified member is already a member of the sorted set, the score is
     * updated and the element reinserted at the right position to ensure the correct ordering. If key does not exist, a
     * new sorted set with the specified members as sole members is created, like if the sorted set was empty. If the
     * key exists but does not hold a sorted set, an error is returned.
     *
     * The score values should be the string representation of a numeric value, and accepts double precision floating
     * point numbers.
     *
     * @param key
     * @param scoreMembers
     * @return The number of elements added to the sorted sets, not including elements already existing for which the
     * score was updated.
     * @throws Exception
     */
    public Long zadd(String key, Map<Double, String> scoreMembers)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Long result = client.zadd(key, scoreMembers);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * If member already exists in the sorted set adds the increment to its score and updates the position of the
     * element in the sorted set accordingly. If member does not already exist in the sorted set it is added with
     * increment as score (that is, like if the previous score was virtually zero). If key does not exist a new sorted
     * set with the specified member as sole member is crated. If the key exists but does not hold a sorted set value an
     * error is returned. <p> The score value can be the string representation of a double precision floating point
     * number. It's possible to provide a negative value to perform a decrement. <p> For an introduction to sorted sets
     * check the Introduction to Redis data types page. <p> Time complexity O(log(N)) with N being the number of
     * elements in the sorted set
     *
     * @param key
     * @param score
     * @param member
     * @return The new score
     */
    public Double zincr(String key, double score, String member)
            throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Double result = client.zincrby(key, score, member);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Return the rank (or index) or member in the sorted set at key, with scores being ordered from low to high. <p>
     * When the given member does not exist in the sorted set, the special value 'nil' is returned. The returned rank
     * (or index) of the member is 0-based for both commands. <p> <b>Time complexity:</b> <p> O(log(N))
     *
     * @see #zrevrank(String, String)
     *
     * @param key
     * @param member
     * @return Integer reply or a nil bulk reply, specifically: the rank of the element as an integer reply if the
     * element exists. A nil bulk reply if there is no such element.
     */
    public Long zrank(String key, String member) throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Long result = client.zrank(key, member);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Return the rank (or index) or member in the sorted set at key, with scores being ordered from high to low. <p>
     * When the given member does not exist in the sorted set, the special value 'nil' is returned. The returned rank
     * (or index) of the member is 0-based for both commands. <p> <b>Time complexity:</b> <p> O(log(N))
     *     
* @see #zrank(String, String)
     *     
* @param key
     * @param member
     * @return Integer reply or a nil bulk reply, specifically: the rank of the element as an integer reply if the
     * element exists. A nil bulk reply if there is no such element.
     */
    public Long zrevrank(final String key, final String member) throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Long result = client.zrevrank(key, member);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Returns the specified range of elements in the sorted set stored at key. The elements are considered to be
     * ordered from the highest to the lowest score. Descending lexicographical order is used for elements with equal
     * score.
     *
     * Apart from the reversed ordering, ZREVRANGE is similar to ZRANGE.
     *
     * @param key
     * @param start
     * @param end
     * @return
     * @throws Exception
     */
    public Set<String> zrevrange(final String key, final long start, final long end) throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Set<String> result = client.zrevrange(key, start, end);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    public Set<Tuple> zrangeWithScores(final String key, final long start, final long end) throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Set<Tuple> result = client.zrangeWithScores(key, start, end);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    public Set<Tuple> zrevrangeWithScores(final String key, final long start, final long end) throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Set<Tuple> result = client.zrevrangeWithScores(key, start, end);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }

    /**
     * Return the score of the specified element of the sorted set at key. If the specified element does not exist in
     * the sorted set, or the key does not exist at all, a special 'nil' value is returned. <p> <b>Time complexity:</b>
     * O(1)
     *
     * @param key
     * @param member
     * @return the score
     */
    public Double zscore(final String key, final String member) throws Exception {
        Jedis client = null;
        try {
            client = borrowClient();
            Double result = client.zscore(key, member);
            returnClient(client);

            return result;
        } catch (Exception ex) {
            invalidClient(client);
            throw ex;
        }
    }
}
